package com.nebula.core.config.mybatis;

import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.StrUtil;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.nebula.core.constant.Constant;
import com.nebula.core.jdbc.NebulaResultSetMetaData;
import com.vesoft.nebula.client.graph.data.*;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.ClassUtils;
import org.apache.ibatis.executor.resultset.DefaultResultSetHandler;
import org.apache.ibatis.executor.resultset.ResultSetHandler;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.ResultMap;
import org.apache.ibatis.plugin.*;
import org.springframework.beans.BeanUtils;
import org.springframework.cglib.beans.BeanMap;
import org.springframework.jdbc.support.JdbcUtils;

import java.beans.PropertyDescriptor;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.Field;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.Statement;
import java.util.*;

/**
 * edge、path的对象，可以通过设置属性名称为src、dst、rank、edge，自动映射到对应对象的属性字段
 */
@SuppressWarnings("all")
@Slf4j
@Intercepts(@Signature(type = ResultSetHandler.class, method = "handleResultSets", args = Statement.class))
public class NebulaResultSetIntercept implements Interceptor {

    private static ObjectMapper objectMapper = new ObjectMapper();

    static {
        // 允许pojo中有在json串中不存在的字段
        objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
        // 允许有注释
        objectMapper.configure(JsonParser.Feature.ALLOW_COMMENTS, true);
    }

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        long start = System.currentTimeMillis();
        DefaultResultSetHandler target = (DefaultResultSetHandler) invocation.getTarget();
        Field field = target.getClass().getDeclaredField("mappedStatement");
        field.setAccessible(true);
        MappedStatement mappedStatement = (MappedStatement) field.get(target);
        List<ResultMap> resultMaps = mappedStatement.getResultMaps();
        boolean isMapValueWrapper = false;
        if (resultMaps != null && !resultMaps.isEmpty()) {
            if (resultMaps.size() > 1) {
                // 仅支持返回一种对象类型
                resultMaps.forEach(r -> log.error(r.getId()));
                throw new RuntimeException("不支持配置多项ResultMap或ResultType！");
            }
            for (ResultMap resultMap : resultMaps) {
                // 返回对象类
                Class<?> aClass = resultMap.getType();
                Statement statement = (Statement) invocation.getArgs()[0];
                // sql返回值
                ResultSet resultSet = statement.getResultSet();

                ResultSetMetaData metaData = resultSet.getMetaData();
                if (!(metaData instanceof NebulaResultSetMetaData)) {
                    return invocation.proceed();
                }

                if (!resultSet.next()) {
                    return invocation.proceed();
                }

                Set<Map<String, Object>> nodes = new HashSet<>();
                List<Map<String, Object>> links = new ArrayList<>();

                // 每条数据的列数
                int columnCount = metaData.getColumnCount();
                List<Object> lists = new ArrayList<>();
                List<com.vesoft.nebula.client.graph.data.ResultSet.Record> recordList = new ArrayList<>();
                do {
                    Map<String, Object> beanMap = new HashMap<>();

                    for (int index = 1; index <= columnCount; index++) {
                        // 当前列名
                        String column = JdbcUtils.lookupColumnName(metaData, index);
                        // 当前列的值
                        Object val = JdbcUtils.getResultSetValue(resultSet, index);
                        if (val == null) {
                            continue;
                        }

                        //nebula的返回值都是ValueWrapper，根据返回类使用不同方式解析
                        if (ValueWrapper.class.isAssignableFrom(val.getClass())) {
                            ValueWrapper wrapper = (ValueWrapper) val;
                            //返回值类型是Map
                            if (Map.class.isAssignableFrom(aClass)) {
                                isMapValueWrapper = true;
                                Map<String, Object> data = new HashMap<>(8);
                                if (wrapper.isVertex()) {
                                    nodes.add(getNode(wrapper.asNode()));
                                } else if (wrapper.isEdge()) {
                                    links.add(getRelationship(wrapper.asRelationship()));
                                } else if (wrapper.isPath()) {
                                    PathWrapper path = wrapper.asPath();
                                    for (PathWrapper.Segment segment : path.getSegments()) {
                                        nodes.add(getNode(segment.getStartNode()));
                                        nodes.add(getNode(segment.getEndNode()));
                                        links.add(getRelationship(segment.getRelationShip()));
                                    }
                                } else if (wrapper.isList()) {
                                    List<ValueWrapper> list = wrapper.asList();
                                    for (ValueWrapper v : list) {
                                        if (v.isVertex()) {
                                            nodes.add(getNode(v.asNode()));
                                        } else if (v.isEdge()) {
                                            links.add(getRelationship(v.asRelationship()));
                                        }
                                    }
                                } else if (wrapper.isMap()) {
                                    HashMap<String, ValueWrapper> map = wrapper.asMap();
                                    Map<String, Object> nodeData = new HashMap<>(8);
                                    for (Map.Entry<String, ValueWrapper> entry : map.entrySet()) {
                                        nodeData.put(entry.getKey(), getValue(entry.getValue()));
                                    }
                                    nodes.add(nodeData);
                                } else {
                                    data.put(column, getValue(wrapper));
                                }
                                if (!data.isEmpty()) {
                                    nodes.add(data);
                                }
                            } else {
                                if (wrapper.isVertex()) {
                                    beanMap.put(column, getNode(wrapper.asNode()));
                                } else if (wrapper.isEdge()) {
                                    beanMap.put(column, getRelationship(wrapper.asRelationship()));
                                } else if (wrapper.isPath()) {
                                    PathWrapper path = wrapper.asPath();
                                    List<Object> relationList = new ArrayList<>();
                                    for (PathWrapper.Segment segment : path.getSegments()) {
                                        relationList.add(getPath(segment, aClass));
                                    }
                                    beanMap.put(column, relationList);
                                } else {
                                    resultSet.beforeFirst();
                                    return invocation.proceed();
                                }
                            }
                        } else {
                            resultSet.beforeFirst();
                            return invocation.proceed();
                        }
                    }
                    if (!isMapValueWrapper) {
                        if (columnCount == 1) {
                            beanMap.values().stream().forEach(v -> {
                                try {
                                    String json = objectMapper.writeValueAsString(v);
                                    lists.add(objectMapper.readValue(json, aClass));
                                } catch (JsonProcessingException e) {
                                    log.error("nebula查询对象映射出错", e);
                                }
                            });
                        } else {
                            String json = objectMapper.writeValueAsString(beanMap);
                            lists.add(objectMapper.readValue(json, aClass));
                        }
                    }

                } while (resultSet.next());
                log.debug("interceptor 消耗时间 ： " + (System.currentTimeMillis() - start));

                if (isMapValueWrapper) {
                    Map<String, Object> graph = new HashMap<>(8);
                    graph.put("nodes", nodes);
                    graph.put("edges", links);
                    lists.add(graph);
                }
                return lists;
            }
        }
        return invocation.proceed();
    }


    private <T> T getNode(Node node, Class<T> clazz) throws UnsupportedEncodingException, InstantiationException, IllegalAccessException {
        if (Object.class.equals(clazz)
                || String.class.equals(clazz)
                || ClassUtils.isPrimitiveOrWrapper(clazz)) {
            return (T) getValue(node.getId());
        }
        Map<String, Object> nodeMap = new HashMap<>(8);
        for (String name : node.tagNames()) {
            nodeMap.put("type", name);
            Map<String, ValueWrapper> props = node.properties(name);
            for (Map.Entry<String, ValueWrapper> entry : props.entrySet()) {
                nodeMap.put(StrUtil.toCamelCase(entry.getKey()), getValue(entry.getValue()));
            }
        }
        nodeMap.put("id", getValue(node.getId()));
        nodeMap.put("vid", getValue(node.getId()));
        T bean = clazz.newInstance();
        BeanMap beanMap = BeanMap.create(bean);
        beanMap.putAll(nodeMap);
        return bean;
    }

    private <T> T getRelationship(Relationship relationship, Class<T> clazz) throws UnsupportedEncodingException, IllegalAccessException, InstantiationException {
        T bean = clazz.newInstance();
        if (relationship.dstId().isEmpty() || relationship.srcId().isEmpty()) {
            return bean;
        }
        Map<String, Object> nodeMap = new HashMap<>(8);
        Map<String, ValueWrapper> props = relationship.properties();
        nodeMap.put(Constant.PATH_SRC, getValue(relationship.srcId()));
        nodeMap.put(Constant.PATH_DST, getValue(relationship.dstId()));
        nodeMap.put(Constant.PATH_RANK, relationship.ranking());
        nodeMap.put(Constant.PATH_EDGE, relationship.edgeName());
        for (Map.Entry<String, ValueWrapper> entry : props.entrySet()) {
            nodeMap.put(StrUtil.toCamelCase(entry.getKey()), getValue(entry.getValue()));
        }
        BeanMap beanMap = BeanMap.create(bean);
        beanMap.putAll(nodeMap);
        return bean;
    }

    private <T> T getPath(PathWrapper.Segment segment, Class<T> clazz) throws UnsupportedEncodingException, IllegalAccessException, InstantiationException {
        T bean = clazz.newInstance();

        Map<String, Object> nodeMap = new HashMap<>(8);
        PropertyDescriptor src = BeanUtils.getPropertyDescriptor(clazz, "src");
        if (src != null) {
            nodeMap.put("src", getNode(segment.getStartNode(), src.getPropertyType()));
        }

        PropertyDescriptor dst = BeanUtils.getPropertyDescriptor(clazz, "dst");
        if (dst != null) {
            nodeMap.put("dst", getNode(segment.getEndNode(), dst.getPropertyType()));
        }

        PropertyDescriptor edge = BeanUtils.getPropertyDescriptor(clazz, "edge");
        if (edge != null) {
            nodeMap.put("edge", getRelationship(segment.getRelationShip(), edge.getPropertyType()));
        }
        BeanMap beanMap = BeanMap.create(bean);
        beanMap.putAll(nodeMap);
        return bean;
    }

    private Map<String, Object> getNode(Node node)
            throws UnsupportedEncodingException {
        Map<String, Object> nodeMap = new HashMap<>(8);
        for (String name : node.tagNames()) {
            Map<String, ValueWrapper> props = node.properties(name);
            for (Map.Entry<String, ValueWrapper> entry : props.entrySet()) {
                nodeMap.put(StrUtil.toCamelCase(entry.getKey()), getValue(entry.getValue()));
            }
        }
        nodeMap.put("id", getValue(node.getId()));
        nodeMap.put("vid", getValue(node.getId()));
        return nodeMap;
    }

    private Map<String, Object> getRelationship(Relationship relationship) throws UnsupportedEncodingException {
        Map<String, Object> link = new HashMap<>(8);
        if (relationship.dstId().isEmpty() || relationship.srcId().isEmpty()) {
            return link;
        }
        link.put("src", getValue(relationship.srcId()));
        link.put("dst", getValue(relationship.dstId()));
        link.put("edge", relationship.edgeName());
        link.put("ranking", relationship.ranking());
        Map<String, ValueWrapper> props = relationship.properties();
        for (Map.Entry<String, ValueWrapper> entry : props.entrySet()) {
            link.put(StrUtil.toCamelCase(entry.getKey()), getValue(entry.getValue()));
        }
        return link;
    }

    private Object getValue(ValueWrapper value) throws UnsupportedEncodingException {
        if (value.isLong()) {
            return value.asLong();
        } else if (value.isBoolean()) {
            return value.asBoolean();
        } else if (value.isDouble()) {
            return value.asDouble();
        } else if (value.isString()) {
            return value.asString();
        } else if (value.isTime()) {
            TimeWrapper timeWrapper = value.asTime();
            return DateUtil.parse(String.format("%02d:%02d:%02d",
                    timeWrapper.getHour(), timeWrapper.getMinute(), timeWrapper.getSecond()));
        } else if (value.isDate()) {
            DateWrapper dateWrapper = value.asDate();
            return DateUtil.parse(dateWrapper.toString());
        } else if (value.isDateTime()) {
            DateTimeWrapper dateTimeWrapper = value.asDateTime();
            return DateUtil.parse(String.format("%d-%02d-%02d %02d:%02d:%02d +0%d00",
                    dateTimeWrapper.getYear(), dateTimeWrapper.getMonth(), dateTimeWrapper.getDay(),
                    dateTimeWrapper.getHour(), dateTimeWrapper.getMinute(), dateTimeWrapper.getSecond(), dateTimeWrapper.getTimezoneOffset() / 60 / 60),
                    "yyyy-MM-dd HH:mm:ss Z");
        } else if (value.isVertex()) {
            return value.asNode();
        } else if (value.isEdge()) {
            return value.asRelationship();
        } else if (value.isPath()) {
            return value.asPath();
        } else if (value.isMap()) {
            return value.asMap();
        } else if (value.isEmpty() || value.isNull()) {
            return "";
        } else {
            return value.getValue();
        }
    }


    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

    @Override
    public void setProperties(Properties properties) {
    }
}
